package com.user_hosted_storage.server.utils;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

public abstract class PropertiesParser {
	
	protected Logger log = Logger.getLogger("properties");
	
	protected Properties properties ;
	private Properties tempProperties;

	// the properties file
	private File propertiesFile ;
	
	private long lastLoadTime;
	
	// Regular expression
	// Every value associated to a key in the file will be parsed to check it is not empty
	private Pattern whitespacesPattern = Pattern.compile("\\s*");
	private Matcher m ;
	
	
	public enum ReloadStatus {
		/** NO_NEED_TO_RELOAD = File has not changed on file system */
		NO_NEED_TO_RELOAD,
		/** FAILED_TO_RELOAD = File has changed on file system but failed to load the new file */
		FAILED_TO_RELOAD, 
		/** RELOADED = File has changed on file system and has been reloaded */
		RELOADED;
	}
	
	protected PropertiesParser() {}
	
	/**
	 * Creates a parser for a properties file.
	 * @param fullPath the path to the directory where <tt>filename</tt> is located
	 * @param filename name of the properties file
	 * @throws IOException if an error occured while reading from the input stream
	 * @throws FileNotFoundException if the file cannot be found
	 * @throws PropertyException 
	 */
	protected PropertiesParser(String fullPath, String filename) throws FileNotFoundException, PropertyException, IOException {
		this(fullPath, filename, null);		
	}
	
	/**
	 * Creates a parser for a properties file.
	 * This constructor is for properties file located in the default properties directory.
	 * @param filename name of the properties file
	 * @throws IOException if an error occured while reading from the input stream
	 * @throws FileNotFoundException if the file cannot be found
	 * @throws PropertyException 
	 */
	protected PropertiesParser (String filename) throws FileNotFoundException, IOException, PropertyException {
		this(null, filename, null);
	}	
	
	/**
	 * Creates a parser for a properties file.
	 * This constructor is for properties file located in the default properties directory.
	 * @param filename name of the properties file
	 * @param logger
	 * @throws IOException if an error occured while reading from the input stream
	 * @throws FileNotFoundException if the file cannot be found
	 * @throws PropertyException 
	 */
	protected PropertiesParser (String filename, Logger logger) throws FileNotFoundException, IOException, PropertyException {
		this(null, filename, logger);
	}
	
	/**
	 * Creates a parser for a properties file.
	 * @param fullPath the path to the directory where <tt>filename</tt> is located
	 * @param filename name of the properties file
	 * @param logger (if null, there is a default Logger)
	 * @throws IOException if an error occured while reading from the input stream
	 * @throws FileNotFoundException if the file cannot be found
	 * @throws PropertyException 
	 */
	protected PropertiesParser(String fullPath, String filename, Logger logger) throws FileNotFoundException, PropertyException, IOException {		
		initParserUsingFile(fullPath, filename, logger);
	}

	/**
	 * @param fullPath
	 * @param filename
	 * @param logger
	 * @throws IOException
	 * @throws FileNotFoundException
	 * @throws PropertyException
	 */
	protected void initParserUsingFile(String fullPath, String filename,
			Logger logger) throws IOException, FileNotFoundException,
			PropertyException {
		
		if (logger != null) {
			log = logger;
		}
		
		String fullFilename;
		String pathToFile;
		
		if (fullPath == null) {
			String defaultPropDir = System.getProperty("PROP_DIR");
			if (defaultPropDir != null) {
				pathToFile = defaultPropDir;
			} else {
				pathToFile = ".";
			}
		} else {
			pathToFile = fullPath;
		}
		
		if (pathToFile.endsWith(File.separator)) {
			fullFilename = pathToFile + filename;
		} else {
			fullFilename = pathToFile + File.separator + filename;
		}
		
		log.info("\t\t\t ==================================================");
		log.info("properties file is '" + fullFilename + "'");
		
		propertiesFile = new File(fullFilename);
		tempProperties = new Properties();
		properties = new Properties ();
		properties.load(new FileInputStream(propertiesFile));
		lastLoadTime = propertiesFile.lastModified();
		
		if (log.isEnabledFor(Level.DEBUG)) {
			log.debug("Last modified date: " + new Date(lastLoadTime));
		}
		
		autosetProperties(true);
	}
	
	/**
	 * Creates a parser for a properties object
	 * @param properties
	 * @throws PropertyException 
	 */
	protected PropertiesParser (Properties prop) throws PropertyException {
		initParserUsingProperties(prop);
	}
	
	/**
	 * @param prop
	 * @throws PropertyException
	 */
	protected void initParserUsingProperties(Properties prop) throws PropertyException {
		properties = prop ;
		autosetProperties(true);
	}
	
	/**
	 * Initializes the properties parser object using the given properties.
	 * @param prop - The properties used for initialization.
	 * @param logPropertiesValues - If true, the properties (key and value) will be written to the properties log. 
	 * <br>If false, the properties values will not be written to a log.
	 * <br>Set this value to false if the properties contain sensitive information (e.g., passwords)
	 * @throws PropertyException
	 */
	protected void initParserUsingProperties(Properties prop, boolean logPropertiesValues) throws PropertyException {
		properties = prop ;
		autosetProperties(logPropertiesValues);
	}
	
	
	
	@SuppressWarnings("unchecked")
	public <T extends PropertiesParser> T getReloadedProperties() {
		reloadProperties();
		return (T) this;
	}
	
	
	/**
	 * Check on file system if properties file has changed and loads the new properties
	 * in memory if necessary
	 * @return NO_NEED_TO_RELOAD if properties file has not changed since last load,<br>
	 * FAILED_TO_RELOAD if file has changed but has not been reloaded due to an exception, <br>
	 * RELOADED if file has changed and properties has been successfully loaded in memory
	 */
	protected ReloadStatus reloadProperties() {
		
		ReloadStatus status = loadPropertiesFile();
		
		if (status == ReloadStatus.RELOADED) {
			try {
				autosetProperties(true);
			} catch (PropertyException e) {
				log.warn("File '" + propertiesFile.getName() + "' has changed on file system but there was an error getting at least one of the new properties: " + e.getMessage());
				return ReloadStatus.FAILED_TO_RELOAD;
			}
		}
		
		return status;
		
	}



	/**
	 * Check on file system if properties file has changed and loads the new properties
	 * in memory if necessary.<br>
	 * Only the object {@link #properties} is affected. Each of the properties defined in the
	 * inherited class of {@link PropertiesParser} are not loaded. Use the method {@link #reloadProperties()}
	 * if you wish to reload each one of the defined properties.
	 * @return NO_NEED_TO_RELOAD if properties file has not changed since last load,<br>
	 * FAILED_TO_RELOAD if file has changed but has not been reloaded due to an exception, <br>
	 * RELOADED if file has changed and properties has been successfully loaded in memory
	 */
	protected ReloadStatus loadPropertiesFile() {
		
		long lastModifiedTime;
		
		try {
			if (propertiesFile == null) {
				log.warn("Properties are not reloadable since they come from Properties object");
				return ReloadStatus.NO_NEED_TO_RELOAD;
			}
			
			lastModifiedTime = propertiesFile.lastModified();
			
			if (lastModifiedTime <= lastLoadTime) {
				if (log.isEnabledFor(Level.TRACE)) {
					log.trace("No need to reload properties");
				}
				return ReloadStatus.NO_NEED_TO_RELOAD;
			}
			
			if (log.isEnabledFor(Level.DEBUG)) {
				log.debug("'" + propertiesFile.getName() + "' Last modified: " + new Date(lastModifiedTime) + ", last load: " + new Date(lastLoadTime));
				log.debug("Reload properties");
			}
			
			tempProperties.clear();
			tempProperties.load(new FileInputStream(propertiesFile));
		} catch (Exception e) {
			log.warn("File '" + propertiesFile.getName() + "' has changed on file system but there was an error getting new properties: " + e.getMessage());
			return ReloadStatus.FAILED_TO_RELOAD;
		}
		
		properties = tempProperties;
		lastLoadTime = lastModifiedTime;
		log.info("\t\t\t ==================================================");
		log.warn("File '" + propertiesFile.getName() + "' has changed on file system. Getting new properties");
		return ReloadStatus.RELOADED;
		
	}
		
		
	/**
	 * If the subclass of PropertiesParser has reloadable properties, 
	 * it must set these properties in this method by reading them from the
	 * member {@link #properties}
	 * @throws PropertyException
	 */
	private void autosetProperties(boolean logPropertiesValues) throws PropertyException {
		
		if (propertiesFile != null) {// Check if properties are coming from file or from object
			log.info("Setting properties to the values written in file '" + propertiesFile.getName() + "'");
		}
		
		boolean atLeastOneFailed = false;
		
		// Go over every field of the PropertiesParser
		Field[] fields = getClass().getFields();
		for (int i = 0; i < fields.length; i++) {
						
			Field field = fields[i];
			if (log.isEnabledFor(Level.TRACE)) {
				log.trace("Field '" + field.getName() + "'");
			}
			
			/* Only operate on fields:
			 * 		1. 'public'  
			 * 		2. not 'final'
			 * 		3. not 'static'
			 */ 
			int fieldModifiers = field.getModifiers();
			if (	!Modifier.isPublic(fieldModifiers) || 
					Modifier.isFinal(fieldModifiers) || 
					Modifier.isStatic(fieldModifiers)) {
				continue;
			}
			
			Class<?> fieldType = field.getType();
			
			Object curPropertyValue = null;
			
			/* Read the new property value */
			try {
				
				curPropertyValue = field.get(this); // the current value of the property
				Object newPropertyValue = get(field.getName(), fieldType); // the new value to affect to the property
				
				if (log.isEnabledFor(Level.INFO)) {					
					String propertyStr = "    <<<< Previous value was 'null'"; // First load of the properties  
					if (curPropertyValue != null) { // was already loaded				
						propertyStr = (curPropertyValue.equals(newPropertyValue))?
										"":("    <<<< Previous value was " + curPropertyValue); 
					}						
					String formattedLog = String.format("%-45s = %-35s %-40s", field.getName(), "'" + newPropertyValue + "'", propertyStr);
					if(logPropertiesValues){
						log.info(formattedLog);
					}
				}
				
				field.set(this, newPropertyValue);
				
			} catch (Exception e) {
				log.error("Error on field '" + field.getName() + "' It will keep its previous value = " + curPropertyValue, e);
				atLeastOneFailed = true;
				continue;
			}
			
		}
		
		log.info("\t\t\t ==================================================\n");
		
		if (atLeastOneFailed) {
			throw new PropertyException();
		}
		
	}

	

	/**
	 * Returns the value to which the specified key is mapped
	 * @param key the properties desired
	 * @param className the class of properties <tt>key</tt> 
	 * 			(supported String, int, long, boolean)
	 * @return the value to which the key is mapped
	 * @throws PropertyNotFoundException if the key is not found. If the property is a boolean no exception is thrown;
	 * instead the boolean is set to 'false'
	 * @throws PropertyEmptyException if no value is associated to the key 
	 * @throws PropertyFormatException if the value mapped to the key is not of type <tt>className</tt>
	 */
	@SuppressWarnings("unchecked")
	protected<T> T get (String key, Class<T> clazz) throws PropertyNotFoundException, 
												PropertyEmptyException, PropertyFormatException {
		
		
				
		Object result;
		
		if (clazz.equals(String[].class)){
			result = getStringsArray(key);
			return (T)result;
		} 
		
		if(clazz.equals(Integer[].class)){
			result = getIntegersArray(key);
			return (T)result;
		}
		
		
		/*
		 * If the property is not in the properties file throw
		 * a PropertyNotFoundException, except for the case of a boolean property
		 * where the property is set to false if not defined in the properties file
		 */
		if (!properties.containsKey(key)) {
			if (clazz == boolean.class) {
				if(null != propertiesFile){
					log.warn("The property '" + key + "' is not defined in properties file '" +	propertiesFile.getAbsolutePath() + "'. It is set to FALSE.");	
				}else{
					log.warn("The property '" + key + "' is not defined in the given properties. It is set to FALSE.");
				}
				
				return (T) Boolean.FALSE;
			}
			throw new PropertyNotFoundException("Property '" + key + "' not found");
		}
		
		String value = (String) properties.get(key);
		
		m = whitespacesPattern.matcher(value);
		if (m.matches()) {
			throw new PropertyEmptyException("No value associated to property '" + key + "'");
		}
		
		if (log.isEnabledFor(Level.TRACE)) {
			log.trace(key + " = " + value);
		}
		
		if (clazz == String.class) {
			result = value;
		} else if (clazz == int.class) {
			result = Integer.parseInt(value);
		} else if (clazz == long.class) {
			result = Long.parseLong(value);
		} else if (clazz == double.class) {
			result = Double.parseDouble(value);
		} else if (clazz == boolean.class) {
			result = Boolean.parseBoolean(value.trim());
		} else {
			throw new PropertyFormatException("Type '" + clazz.getClass().getName() + "' is not supported for property '" + key + "'");
		}
		
		try {
			return (T) result;
		} catch (ClassCastException e) {
			throw new PropertyFormatException("property '" + key + "' is not of type '" + clazz.getName() + "'");
		}
		
	}


	private String[] getStringsArray(String key) throws PropertyEmptyException, PropertyFormatException {
		boolean noMoreListValues = false;
		List<String> values = new LinkedList<String>();
		int count = 1;
		
		while(!noMoreListValues){
			
			try {
				values.add(get(key+"_"+count,String.class));
			} catch (PropertyNotFoundException e) {
				noMoreListValues = true;
			}
			count++;
		}
		String[] result = new String[values.size()];
		values.toArray(result);
		return result;
	}
	
	private Integer[] getIntegersArray(String key) throws PropertyEmptyException, PropertyFormatException {
		boolean noMoreListValues = false;
		List<Integer> values = new LinkedList<Integer>();
		int count = 1;
		
		while(!noMoreListValues){
			
			try {
				values.add(get(key+"_"+count,int.class));
			} catch (PropertyNotFoundException e) {
				noMoreListValues = true;
			}
			count++;
		}
		Integer[] result = new Integer[values.size()];
		values.toArray(result);
		return result;
	}
	
	public void setProperties(Properties props) throws PropertyException{
		properties = props;
		
		autosetProperties(false);
	}
	
	
}
